package com.cly.service.impl;

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ZipUtil;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.cly.base.contants.CacheKeysConstants;
import com.cly.dto.FileChunkDto;
import com.cly.entity.File;
import com.cly.enums.FileCategoryEnum;
import com.cly.mapper.FileMapper;
import com.cly.note.ExcludeMethods;
import com.cly.properties.FileProperties;
import com.cly.result.JsonRequest;
import com.cly.result.exception.ServiceException;
import com.cly.result.exenum.RequestException;
import com.cly.service.IFileService;
import com.cly.utils.FileUtils;
import com.cly.utils.JsonSerializeUtils;
import com.cly.utils.RedisClientsUtils;
import lombok.Cleanup;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.apache.commons.codec.digest.DigestUtils;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;

import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

/**
 * 文件业务层实现
 *
 * @author by 春风能解释
 * <p>
 * 2024/8/29
 */
@Service
@RequiredArgsConstructor
public class FileServiceImpl extends ServiceImpl<FileMapper, File> implements IFileService {

    /**
     * 文件配置
     */
    private final FileProperties fileProperties;
    /**
     * redis工具类
     */
    private final RedisClientsUtils redisClient;

    /**
     * 分页查询文件
     *
     * @param current 页码
     * @param size    页长
     * @param file    条件
     * @return JSON
     */
    @Override
    @Cacheable(value = CacheKeysConstants.FileKeys.FILE_ARRAY, key = "#root.args")
    public JsonRequest<List<File>> pageFile(Integer current, Integer size, File file) {
        IPage<File> fileIPage = baseMapper.selectPageFile(new Page<>(current, size), file);
        // 传入分页
        return JsonRequest.success(fileIPage.getRecords(), fileIPage.getTotal());
    }

    /**
     * 根据ID查询文件信息
     *
     * @param id 文件ID
     * @return JSON
     */
    @Override
    @Cacheable(value = CacheKeysConstants.FileKeys.FILE_ARRAY, key = "#id")
    public JsonRequest<File> getOne(Long id) {
        return JsonRequest.success(getById(id));
    }

    /**
     * 文件上传并返回访问路径
     *
     * @param file 文件
     * @return JSON
     */
    @Override
    @SneakyThrows
    @CacheEvict(value = {CacheKeysConstants.FileKeys.FILE_ARRAY, CacheKeysConstants.FileKeys.FILE_TYPE}, allEntries = true)
    public JsonRequest<Long> uploadFile(MultipartFile file) {
        if (file == null) {
            throw new ServiceException(RequestException.FILE_ERROR);
        }
        // 文件MD5
        String md5Hex = DigestUtils.md5Hex(file.getBytes());
        // 拿到文件
        File data = getOne(Wrappers.lambdaQuery(File.class)
                .eq(File::getFileMd5, md5Hex));
        // 文件存在直接返回
        if (Objects.nonNull(data)) {
            return JsonRequest.success(data.getId());
        }
        String fileType = FileUtils.getFileType(file.getOriginalFilename());
        if (ArrayUtil.isNotEmpty(fileProperties.getFileAllowType()) &&
                !ArrayUtil.contains(fileProperties.getFileAllowType(), fileType)) {
            throw new ServiceException(RequestException.FILE_TYPE_ERROR);
        }
        // 添加到数据库
        long id = IdWorker.getId();
        String storePath = FileUtils.fileStore(file, String.valueOf(id));
        File fileData;
        fileData = new File()
                .setFileName(FileUtils.getFileName(file.getOriginalFilename()))
                .setFileCategory(FileUtils.getType(fileType))
                .setFileSuffix(fileType)
                .setFileSize(file.getSize())
                .setFilePath(storePath)
                .setFileMd5(md5Hex);
        fileData.setId(id);
        baseMapper.insert(fileData);
        return JsonRequest.success(id);
    }


    /**
     * 添加文件备注
     *
     * @param file 数据
     * @return JSON
     */
    @Override
    @CacheEvict(value = {CacheKeysConstants.FileKeys.FILE_ARRAY, CacheKeysConstants.FileKeys.FILE_TYPE}, allEntries = true)
    public JsonRequest<Integer> addNote(File file) {
        int column = baseMapper.update(Wrappers.lambdaUpdate(File.class)
                .eq(File::getId, file.getId())
                .set(File::getFileNote, file.getFileNote()));
        if (column < 1) {
            throw new ServiceException(RequestException.FILE_ERROR);
        }
        return JsonRequest.success(column);
    }

    /**
     * 根据ID删除文件
     *
     * @param id 文件ID
     * @return JSON
     */
    @Override
    @CacheEvict(value = {CacheKeysConstants.FileKeys.FILE_ARRAY, CacheKeysConstants.FileKeys.FILE_TYPE}, allEntries = true)
    public JsonRequest<Integer> deleteFileById(List<Long> id) {
        List<File> files = listByIds(id);
        if (CollectionUtil.isEmpty(files)) {
            throw new ServiceException(RequestException.NOT_FOUND);
        }
        return JsonRequest.success(baseMapper.deleteByIds(id));
    }

    /**
     * 根据ID文件下载
     *
     * @param id 文件路径
     * @return ResponseEntity
     */
    @Override
    @ExcludeMethods
    public ResponseEntity<StreamingResponseBody> showFile(Long id) {
        File file = redisClient.get(CacheKeysConstants.FileKeys.FILE_ARRAY + "::", id, this::getById, File.class);
        // 缓存中存在
        if (Objects.isNull(file)) {
            throw new ServiceException(RequestException.NOT_FOUND);
        }
        StreamingResponseBody stream = outputStream -> {
            String finalPath = file.getFilePath();
            //绝对路径情况处理
            if (!file.getFilePath().contains(FileUtils.getRootPath())) {
                finalPath = FileUtils.getRootPath() + file.getFilePath();
            }
            try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(finalPath))) {
                byte[] buffer = new byte[1024];
                int bytesRead;
                while ((bytesRead = bis.read(buffer)) != -1) {
                    outputStream.write(buffer, 0, bytesRead);
                }
            } catch (Exception e) {
                throw new ServiceException(e);
            } finally {
                outputStream.flush();
            }
        };
        // 返回响应体
        return ResponseEntity.status(HttpStatus.PARTIAL_CONTENT)
                .contentType(MediaType.APPLICATION_OCTET_STREAM)
                .header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + file.getFileName())
                .contentLength(file.getFileSize())
                .body(stream);
    }

    /**
     * 分片下载文件
     *
     * @param id    文件ID
     * @param range 请求头中的Range
     * @return ResponseEntity
     */
    @Override
    @SneakyThrows
    @ExcludeMethods
    public ResponseEntity<StreamingResponseBody> chunkDownload(Long id, String range) {
        File fileData = redisClient.get(CacheKeysConstants.FileKeys.FILE_ARRAY + "::", id, this::getById, File.class);
        // 缓存中存在
        if (Objects.isNull(fileData)) {
            throw new ServiceException(RequestException.NOT_FOUND);
        }
        String finalPath = fileData.getFilePath();
        //绝对路径情况处理
        if (!fileData.getFilePath().contains(FileUtils.getRootPath())) {
            finalPath = FileUtils.getRootPath() + fileData.getFilePath();
        }
        java.io.File file = new java.io.File(finalPath);
        if (!file.exists()) {
            return ResponseEntity.notFound().build();
        }
        // 文件大小
        long fileSize = file.length();
        //  如果文件小于最大分片大小
        if (fileSize <= fileProperties.getFileMaxChunkSize()) {
            return showFile(id);
        }
        long start, end = fileSize - 1;
        // 判断是否支持断点续传
        if (range != null && range.startsWith("bytes=")) {
            String[] ranges = range.substring(6).split("-");
            start = Long.parseLong(ranges[0]);
            if (ranges.length > 1) {
                end = Long.parseLong(ranges[1]);
            }
        } else {
            start = 0;
        }
        // 判断请求头中的范围是否超出文件大小
        if (start > end || start >= fileSize) {
            return ResponseEntity.status(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE)
                    .contentType(MediaType.APPLICATION_OCTET_STREAM)
                    .contentLength(fileSize)
                    .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + file.getName())
                    .header(HttpHeaders.CONTENT_RANGE, "bytes */" + fileSize)
                    .build();
        }
        // 判断请求头中的范围是否超出文件大小
        end = Math.min(end, fileSize - 1);
        long contentLength = end - start + 1;
        // 创建响应体
        StreamingResponseBody stream = outputStream -> {
            try (RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r")) {
                randomAccessFile.seek(start);
                byte[] buffer = new byte[1024];
                int bytesRead;
                while ((bytesRead = randomAccessFile.read(buffer)) != -1) {
                    outputStream.write(buffer, 0, bytesRead);
                }
            } catch (IOException e) {
                throw new ServiceException(e);
            } finally {
                outputStream.flush();
            }
        };
        // 返回响应体
        return ResponseEntity.status(HttpStatus.PARTIAL_CONTENT)
                .contentType(MediaType.APPLICATION_OCTET_STREAM)
                .contentLength(contentLength)
                .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + file.getName())
                .header(HttpHeaders.CONTENT_RANGE, "bytes " + start + "-" + end + "/" + fileSize)
                .header(HttpHeaders.ACCEPT_RANGES, "bytes")
                .body(stream);
    }

    /**
     * 判断是否为图片
     *
     * @param id 文件ID
     * @return JSON
     */
    @Override
    @Cacheable(value = CacheKeysConstants.FileKeys.FILE_TYPE, key = "#id")
    public JsonRequest<Boolean> isImage(Long id) {
        File file = getById(id);
        return JsonRequest.success(Objects.nonNull(file) && file.getFileCategory().equals(FileCategoryEnum.IMAGE.getValue()));
    }

    /**
     * 检查文件是否已上传
     *
     * @param fileChunk 文件分片传输对象
     * @return JSON
     */
    @Override
    public JsonRequest<Long> checkFile(FileChunkDto fileChunk) {
        File fileData = redisClient.get(CacheKeysConstants.FileKeys.FILE_ARRAY + "::", fileChunk.getIdentifier(), (md5) -> getOne(Wrappers.lambdaQuery(File.class)
                .eq(File::getFileMd5, md5)), File.class);
        if (Objects.isNull(fileData)) {
            return JsonRequest.error(RequestException.NOT_FOUND);
        }
        java.io.File file = new java.io.File(FileUtils.getRootPath() + fileData.getFilePath());
        if (!file.exists()) {
            return JsonRequest.error(RequestException.NOT_FOUND);
        }
        return JsonRequest.success(fileData.getId());
    }

    /**
     * 文件分片上传
     *
     * @param fileChunk 文件分片传输对象
     * @return JSON
     */
    @Override
    public JsonRequest<List<Long>> uploadFileChunk(FileChunkDto fileChunk) {
        String chunkKey = CacheKeysConstants.FileKeys.FILE_CHUNK +
                fileChunk.getIdentifier();
        // 获取已上传的文件块
        String chunkListStr = redisClient.get(chunkKey);
        List<Long> chunkList = null;
        //读取缓存中的文件块
        if (StringUtils.hasText(chunkListStr)) {
            chunkList = JsonSerializeUtils.reSerializeList(chunkListStr, Long.class);
        }
        int status = fileChunk.getChunkNumber() >= fileChunk.getTotalChunks() ? 205 : 200;
        if (Objects.nonNull(chunkList) && chunkList.contains(fileChunk.getChunkNumber())) {
            return JsonRequest.success(status, chunkList);
        }
        // 判断文件类型
        String fileType = FileUtils.getFileType(fileChunk.getFile().getOriginalFilename());
        if (ArrayUtil.isNotEmpty(fileProperties.getFileAllowType())
                && !ArrayUtil.contains(fileProperties.getFileAllowType(), fileType)) {
            throw new ServiceException(RequestException.FILE_TYPE_ERROR);
        }
        // 判断分片是否已经存在
        String id = fileChunk.getIdentifier() + "_"
                + fileChunk.getChunkNumber();
        String path = FileUtils.getTempPath(id, fileChunk.getFilename());
        // 如果存在则移除
        if (FileUtils.isExists(path)) {
            FileUtils.deleteFile(path);
        }
        // 存储文件
        FileUtils.fileStore(fileChunk.getFile(),
                FileUtils.tempFile(),
                id);
        // 判断分片是否已经存在
        if (Objects.isNull(chunkList)) {
            chunkList = new ArrayList<>();
        }
        chunkList.add(fileChunk.getChunkNumber());
        // 去重+排序
        chunkList = chunkList.stream()
                .distinct()
                .sorted()
                .toList();
        // 永久
        redisClient.set(chunkKey, chunkList, true);
        return JsonRequest.success(status, chunkList);
    }

    /**
     * 合并文件
     *
     * @param fileChunk 文件分片传输对象
     * @return JSON
     */
    @SneakyThrows
    @Override
    @CacheEvict(value = {CacheKeysConstants.FileKeys.FILE_ARRAY, CacheKeysConstants.FileKeys.FILE_TYPE}, allEntries = true)
    public JsonRequest<Long> mergeFile(FileChunkDto fileChunk) {
        String filePath;
        List<java.io.File> fileList = new ArrayList<>();
        String type = FileUtils.getFileType(fileChunk.getFilename());
        long id = IdWorker.getId();
        // 判断所有分片是否存在
        for (int index = 1; index <= fileChunk.getTotalChunks(); index++) {
            // 判断分片是否已经存在
            filePath = FileUtils.getTempPath(
                    fileChunk.getIdentifier() + "_" + index,
                    fileChunk.getFilename());
            java.io.File chunk = new java.io.File(FileUtils.getRootPath() + filePath);
            if (!chunk.exists()) {
                return JsonRequest.error(RequestException.FILE_ERROR);
            }
            fileList.add(chunk);
        }
        //最终合并文件后的路径
        String finallyPath =
                FileUtils.getTimeCategory(String.valueOf(id), fileChunk.getFilename());
        // 合并分片
        @Cleanup RandomAccessFile write = new RandomAccessFile(FileUtils.getRootPath() + finallyPath, "rwd");
        byte[] bytes = new byte[1024];
        int len;
        for (java.io.File chunk : fileList) {
            RandomAccessFile read = new RandomAccessFile(chunk, "r");
            while ((len = read.read(bytes)) != -1) {
                write.write(bytes, 0, len);
            }
            // 先关闭文件
            read.close();
            // 删除分片
            chunk.delete();
        }
        // 存入数据库
        File file = new File()
                .setFileMd5(fileChunk.getIdentifier())
                .setFilePath(finallyPath)
                .setFileName(fileChunk.getFilename())
                .setFileSuffix(type)
                .setFileCategory(FileUtils.getType(type))
                .setFileSize(fileChunk.getTotalSize());
        file.setId(id);
        save(file);
        // 移除缓存
        redisClient.remove(CacheKeysConstants.FileKeys.FILE_CHUNK +
                fileChunk.getIdentifier());
        return JsonRequest.success(id);
    }

    /**
     * 批量下载文件
     *
     * @param ids 文件ID
     * @return ResponseEntity
     */
    @Override
    public JsonRequest<Long> zipDownloadFiles(List<Long> ids) {
        List<File> fileList = redisClient.gets(CacheKeysConstants.FileKeys.FILE_ARRAY + "::", ids, this::listByIds, File.class);
        // 缓存中存在
        if (fileList.isEmpty()) {
            throw new ServiceException(RequestException.NOT_FOUND);
        }
        java.io.File[] originFiles = fileList.stream()
                .map(v -> new java.io.File(FileUtils.getRootPath() + v.getFilePath()))
                .toArray(java.io.File[]::new);
        Long id = IdWorker.getId();
        java.io.File zipFile = new java.io.File(FileUtils.getTempPath(String.valueOf(id), ".zip"));
        ZipUtil.zip(zipFile, StandardCharsets.UTF_8, false, originFiles);
        File file = new File()
                .setFileName(zipFile.getName())
                .setFileCategory(FileUtils.getType(zipFile.getPath()))
                .setFileSize(zipFile.length())
                .setFilePath(zipFile.getPath());
        file.setId(id);
        //存入redis并设置过期
        redisClient.set(CacheKeysConstants.FileKeys.FILE_ARRAY + "::" + id, file, CacheKeysConstants.CacheTimes.PERMISSION_LIST);
        return JsonRequest.success(id);
    }
}
